ட்ரீ டிராவர்சலுக்கான ஜெனரிக் விசிட்டர் பேட்டர்னில் தேர்ச்சி பெறுங்கள். நெகிழ்வான, பராமரிக்கக்கூடிய குறியீட்டிற்காக அல்காரிதம்களை ட்ரீ கட்டமைப்பிலிருந்து பிரிப்பதற்கான ஒரு விரிவான வழிகாட்டி.
நெகிழ்வான ட்ரீ டிராவர்சலைத் திறத்தல்: ஜெனரிக் விசிட்டர் பேட்டர்ன் பற்றிய ஒரு ஆழமான பார்வை
மென்பொருள் பொறியியல் உலகில், படிநிலை, மரம் போன்ற கட்டமைப்புகளில் ஒழுங்கமைக்கப்பட்ட தரவுகளை நாம் அடிக்கடி எதிர்கொள்கிறோம். கம்பைலர்கள் நமது குறியீட்டைப் புரிந்துகொள்ளப் பயன்படுத்தும் அப்ஸ்ட்ராக்ட் சின்டாக்ஸ் ட்ரீ (ASTs) முதல், இணையத்தை இயக்கும் டாக்குமெண்ட் ஆப்ஜெக்ட் மாடல் (DOM) வரை, மற்றும் எளிமையான கோப்பு முறைமைகள் கூட, மரங்கள் எல்லா இடங்களிலும் உள்ளன. இந்த கட்டமைப்புகளுடன் பணிபுரியும் போது ஒரு அடிப்படைக் கடமை டிராவர்சல் ஆகும்: ஒவ்வொரு நோடிற்கும் சென்று சில செயல்பாடுகளைச் செய்வது. இருப்பினும், சவால் என்னவென்றால், இதை ஒரு சுத்தமான, பராமரிக்கக்கூடிய மற்றும் விரிவாக்கக்கூடிய வழியில் செய்வதாகும்.
பாரம்பரிய அணுகுமுறைகள் பெரும்பாலும் செயல்பாட்டு தர்க்கத்தை நேரடியாக நோட் வகுப்புகளுக்குள் பொதிந்து விடுகின்றன. இது ஒற்றைப்படையான, இறுக்கமாகப் பிணைக்கப்பட்ட குறியீட்டிற்கு வழிவகுக்கிறது, இது முக்கிய மென்பொருள் வடிவமைப்பு கொள்கைகளை மீறுகிறது. ஒரு புதிய செயல்பாட்டைச் சேர்ப்பது, ஒரு பிரட்டி-பிரிண்டர் அல்லது ஒரு வேலிடேட்டர் போல, ஒவ்வொரு நோட் வகுப்பையும் மாற்றியமைக்க உங்களைத் தூண்டுகிறது, இது கணினியை பலவீனமாகவும் பராமரிக்க கடினமாகவும் ஆக்குகிறது.
கிளாசிக் விசிட்டர் டிசைன் பேட்டர்ன், அல்காரிதம்களை அவை செயல்படும் பொருட்களிலிருந்து பிரிப்பதன் மூலம் ஒரு சக்திவாய்ந்த தீர்வை வழங்குகிறது. ஆனால் கிளாசிக் பேட்டர்னுக்கும் அதன் வரம்புகள் உள்ளன, குறிப்பாக விரிவாக்கத்தன்மையைப் பொருத்தவரை. இங்குதான் ஜெனரிக் விசிட்டர் பேட்டர்ன், குறிப்பாக ட்ரீ டிராவர்சலுக்குப் பயன்படுத்தும்போது, தனித்து நிற்கிறது. ஜெனரிக்ஸ், டெம்ப்ளேட்கள் மற்றும் வேரியண்ட்கள் போன்ற நவீன நிரலாக்க மொழி அம்சங்களைப் பயன்படுத்துவதன் மூலம், எந்தவொரு ட்ரீ கட்டமைப்பையும் செயலாக்க ஒரு மிகவும் நெகிழ்வான, மீண்டும் பயன்படுத்தக்கூடிய மற்றும் சக்திவாய்ந்த அமைப்பை உருவாக்க முடியும்.
இந்த ஆழமான பார்வை உங்களை கிளாசிக் விசிட்டர் பேட்டர்னிலிருந்து ஒரு அதிநவீன, ஜெனரிக் செயலாக்கத்திற்கான பயணத்தில் வழிநடத்தும். நாம் ஆராய்வோம்:
- கிளாசிக் விசிட்டர் பேட்டர்ன் மற்றும் அதன் உள்ளார்ந்த சவால்கள் பற்றிய ஒரு மீள்பார்வை.
- செயல்பாடுகளை இன்னும் கூடுதலாகப் பிரிக்கும் ஒரு ஜெனரிக் அணுகுமுறைக்கான பரிணாமம்.
- ஒரு ஜெனரிக் ட்ரீ டிராவர்சல் விசிட்டரின் விரிவான, படிப்படியான செயலாக்கம்.
- டிராவர்சல் தர்க்கத்தை செயல்பாட்டு தர்க்கத்திலிருந்து பிரிப்பதன் ஆழமான நன்மைகள்.
- இந்த பேட்டர்ன் மகத்தான மதிப்பை வழங்கும் நிஜ-உலகப் பயன்பாடுகள்.
நீங்கள் ஒரு கம்பைலர், ஒரு ஸ்டேடிக் அனாலிசிஸ் கருவி, ஒரு UI ஃபிரேம்வொர்க் அல்லது சிக்கலான டேட்டா ஸ்ட்ரக்சர்களை நம்பியிருக்கும் எந்தவொரு அமைப்பை உருவாக்கினாலும், இந்த பேட்டர்னில் தேர்ச்சி பெறுவது உங்கள் கட்டமைப்பு சிந்தனையையும் உங்கள் குறியீட்டின் தரத்தையும் உயர்த்தும்.
கிளாசிக் விசிட்டர் பேட்டர்னை மீண்டும் பார்வையிடுதல்
ஜெனரிக் பரிணாமத்தைப் பாராட்டுவதற்கு முன்பு, அதன் அடித்தளத்தைப் பற்றிய திடமான புரிதல் நமக்கு இருக்க வேண்டும். "காங் ஆஃப் ஃபோர்" அவர்களின் முக்கிய புத்தகமான Design Patterns: Elements of Reusable Object-Oriented Software-ல் விவரித்தபடி, விசிட்டர் பேட்டர்ன் என்பது ஒரு பிஹேவியரல் பேட்டர்ன் ஆகும், இது தற்போதுள்ள பொருள் கட்டமைப்புகளை மாற்றியமைக்காமல் புதிய செயல்பாடுகளைச் சேர்க்க உங்களை அனுமதிக்கிறது.
அது தீர்க்கும் சிக்கல்
NumberNode (ஒரு நேரடி மதிப்பு) மற்றும் AdditionNode (இரண்டு துணை-கோவைகளின் கூட்டலைக் குறிக்கிறது) போன்ற வெவ்வேறு நோட் வகைகளால் ஆன ஒரு எளிய எண்கணிதக் கோவையின் ட்ரீ இருப்பதாக கற்பனை செய்து பாருங்கள். இந்த ட்ரீ மீது நீங்கள் பல தனித்துவமான செயல்பாடுகளைச் செய்ய விரும்பலாம்:
- மதிப்பீடு (Evaluation): கோவையின் இறுதி எண் முடிவைக் கணக்கிடுதல்.
- அழகுபடுத்தி அச்சிடுதல் (Pretty Printing): "(5 + 3)" போன்ற மனிதர் படிக்கக்கூடிய ஒரு சரம் வடிவத்தை உருவாக்குதல்.
- வகை சரிபார்ப்பு (Type Checking): சம்பந்தப்பட்ட வகைகளுக்கு செயல்பாடுகள் சரியானவையா என்பதைச் சரிபார்த்தல்.
புத்திசாலித்தனமற்ற அணுகுமுறை என்னவென்றால், `evaluate()`, `print()`, மற்றும் `typeCheck()` போன்ற மெத்தடுகளை அடிப்படை `Node` வகுப்பில் சேர்த்து, ஒவ்வொரு கான்கிரீட் நோட் வகுப்பிலும் அவற்றை ஓவர்ரைடு செய்வதாகும். இது நோட் வகுப்புகளை தொடர்பில்லாத தர்க்கத்துடன் நிரப்புகிறது. ஒவ்வொரு முறையும் நீங்கள் ஒரு புதிய செயல்பாட்டைக் கண்டுபிடிக்கும்போது, படிநிலையில் உள்ள ஒவ்வொரு நோட் வகுப்பையும் நீங்கள் தொட வேண்டும். இது திறந்த/மூடிய கொள்கையை (Open/Closed Principle) மீறுகிறது, இது மென்பொருள் கூறுகள் விரிவாக்கத்திற்குத் திறந்திருக்க வேண்டும் ஆனால் மாற்றத்திற்கு மூடப்பட்டிருக்க வேண்டும் என்று கூறுகிறது.
கிளாசிக் தீர்வு: டபுள் டிஸ்பாட்ச்
விசிட்டர் பேட்டர்ன் இந்த சிக்கலை இரண்டு புதிய படிநிலைகளை அறிமுகப்படுத்துவதன் மூலம் தீர்க்கிறது: ஒரு விசிட்டர் படிநிலை மற்றும் ஒரு எலிமெண்ட் படிநிலை (நமது நோட்கள்). இதன் மாயம் டபுள் டிஸ்பாட்ச் எனப்படும் ஒரு நுட்பத்தில் உள்ளது.
முக்கிய பங்களிப்பாளர்கள்:
- எலிமெண்ட் இன்டர்ஃபேஸ் (உதாரணமாக, `Node`): ஒரு `accept(Visitor v)` மெத்தடை வரையறுக்கிறது.
- கான்கிரீட் எலிமெண்ட்கள் (உதாரணமாக, `NumberNode`, `AdditionNode`): `accept` மெத்தடை செயல்படுத்துகின்றன. இதன் செயலாக்கம் எளிமையானது: `visitor.visit(this);`.
- விசிட்டர் இன்டர்ஃபேஸ்: ஒவ்வொரு கான்கிரீட் எலிமெண்ட் வகைக்கும் ஒரு ஓவர்லோட் செய்யப்பட்ட `visit` மெத்தடை அறிவிக்கிறது. உதாரணமாக, `visit(NumberNode n)` மற்றும் `visit(AdditionNode n)`.
- கான்கிரீட் விசிட்டர் (உதாரணமாக, `EvaluationVisitor`, `PrintVisitor`): ஒரு குறிப்பிட்ட செயல்பாட்டைச் செய்ய `visit` மெத்தடுகளை செயல்படுத்துகிறது.
இது எப்படி வேலை செய்கிறது என்பது இங்கே: நீங்கள் `node.accept(myVisitor)` என்று அழைக்கிறீர்கள். `accept`-க்குள், நோட் `myVisitor.visit(this)` என்று அழைக்கிறது. இந்த நேரத்தில், கம்பைலருக்கு `this`-ன் கான்கிரீட் வகை (உதாரணமாக, `AdditionNode`) மற்றும் `myVisitor`-ன் கான்கிரீட் வகை (உதாரணமாக, `EvaluationVisitor`) தெரியும். எனவே அது சரியான `visit` மெத்தடிற்கு டிஸ்பாட்ச் செய்ய முடியும்: `EvaluationVisitor::visit(AdditionNode*)`. இந்த இரண்டு-படி அழைப்பு, ஒரு ஒற்றை விர்ச்சுவல் ஃபங்ஷன் அழைப்பால் செய்ய முடியாததைச் செய்கிறது: இரண்டு வெவ்வேறு பொருட்களின் ரன்டைம் வகைகளின் அடிப்படையில் சரியான மெத்தடைத் தீர்ப்பது.
கிளாசிக் பேட்டர்னின் வரம்புகள்
நேர்த்தியாக இருந்தாலும், கிளாசிக் விசிட்டர் பேட்டர்னில் ஒரு குறிப்பிடத்தக்க குறைபாடு உள்ளது, இது வளரும் கணினிகளில் அதன் பயன்பாட்டைத் தடுக்கிறது: எலிமெண்ட் படிநிலையில் விறைப்புத்தன்மை.
`Visitor` இன்டர்ஃபேஸ் ஒவ்வொரு `ConcreteElement` வகைக்கும் ஒரு `visit` மெத்தடைக் கொண்டுள்ளது. நீங்கள் ஒரு புதிய நோட் வகையைச் சேர்க்க விரும்பினால்—உதாரணமாக, ஒரு `MultiplicationNode`—நீங்கள் அடிப்படை `Visitor` இன்டர்ஃபேஸில் ஒரு புதிய `visit(MultiplicationNode n)` மெத்தடைச் சேர்க்க வேண்டும். இது உங்கள் கணினியில் உள்ள ஒவ்வொரு கான்கிரீட் விசிட்டர் வகுப்பையும் இந்த புதிய மெத்தடைச் செயல்படுத்தப் புதுப்பிக்க உங்களைத் தூண்டுகிறது. புதிய செயல்பாடுகளைச் சேர்ப்பதில் நாம் தீர்த்த அதே சிக்கல், இப்போது புதிய எலிமெண்ட் வகைகளைச் சேர்க்கும்போது மீண்டும் தோன்றுகிறது. கணினி செயல்பாட்டுப் பக்கத்தில் மாற்றத்திற்கு மூடப்பட்டுள்ளது, ஆனால் எலிமெண்ட் பக்கத்தில் பரந்த அளவில் திறந்துள்ளது.
எலிமெண்ட் படிநிலைக்கும் விசிட்டர் படிநிலைக்கும் இடையிலான இந்த சுழற்சி சார்ந்திருத்தல், ஒரு மிகவும் நெகிழ்வான, ஜெனரிக் தீர்வைத் தேடுவதற்கான முதன்மைக் காரணமாகும்.
ஜெனரிக் பரிணாமம்: ஒரு மிகவும் நெகிழ்வான அணுகுமுறை
கிளாசிக் பேட்டர்னின் முக்கிய வரம்பு, விசிட்டர் இன்டர்ஃபேஸுக்கும் கான்கிரீட் எலிமெண்ட் வகைகளுக்கும் இடையிலான நிலையான, கம்பைல்-டைம் பிணைப்பு ஆகும். ஜெனரிக் அணுகுமுறை இந்த பிணைப்பை உடைக்க முயல்கிறது. மைய யோசனை என்னவென்றால், சரியான கையாளும் தர்க்கத்திற்கு டிஸ்பாட்ச் செய்யும் பொறுப்பை, ஓவர்லோட் செய்யப்பட்ட மெத்தடுகளின் ஒரு விறைப்பான இன்டர்ஃபேஸிலிருந்து மாற்றுவதாகும்.
நவீன சி++, அதன் சக்திவாய்ந்த டெம்ப்ளேட் மெட்டாபுரோகிராமிங் மற்றும் `std::variant` போன்ற ஸ்டாண்டர்ட் லைப்ரரி அம்சங்களுடன், இதைச் செயல்படுத்த ஒரு விதிவிலக்காக சுத்தமான மற்றும் திறமையான வழியை வழங்குகிறது. சி# அல்லது ஜாவா போன்ற மொழிகளில் பிரதிபலிப்பு அல்லது ஜெனரிக் இன்டர்ஃபேஸ்களைப் பயன்படுத்தி இதே போன்ற அணுகுமுறையை அடைய முடியும், இருப்பினும் சாத்தியமான செயல்திறன் பரிமாற்றங்களுடன்.
நமது குறிக்கோள் ஒரு அமைப்பை உருவாக்குவதாகும், அங்கு:
- புதிய நோட் வகைகளைச் சேர்ப்பது உள்ளூர்மயமாக்கப்பட்டது மற்றும் தற்போதுள்ள அனைத்து விசிட்டர் செயலாக்கங்களிலும் தொடர்ச்சியான மாற்றங்கள் தேவையில்லை.
- புதிய செயல்பாடுகளைச் சேர்ப்பது எளிமையாகவே உள்ளது, இது விசிட்டர் பேட்டர்னின் அசல் குறிக்கோளுடன் ஒத்துப்போகிறது.
- டிராவர்சல் தர்க்கமே (உதாரணமாக, ப்ரீ-ஆர்டர், போஸ்ட்-ஆர்டர்) ஜெனரிக்காக வரையறுக்கப்பட்டு எந்தவொரு செயல்பாட்டிற்கும் மீண்டும் பயன்படுத்தப்படலாம்.
இந்த மூன்றாவது புள்ளிதான் நமது "ட்ரீ டிராவர்சல் வகை செயலாக்கத்தின்" திறவுகோல். நாம் செயல்பாட்டை டேட்டா ஸ்ட்ரக்சரிலிருந்து பிரிப்பது மட்டுமல்லாமல், டிராவர்ஸ் செய்யும் செயலையும் செயல்படும் செயலையும் பிரிப்போம்.
சி++ இல் ட்ரீ டிராவர்சலுக்கான ஜெனரிக் விசிட்டரை செயல்படுத்துதல்
நமது ஜெனரிக் விசிட்டர் கட்டமைப்பை உருவாக்க நவீன சி++ (சி++17 அல்லது அதற்குப் பிந்தையது) பயன்படுத்துவோம். `std::variant`, `std::unique_ptr`, மற்றும் டெம்ப்ளேட்களின் கலவையானது நமக்கு ஒரு டைப்-சேஃப், திறமையான மற்றும் மிகவும் வெளிப்படையான தீர்வை அளிக்கிறது.
படி 1: ட்ரீ நோட் கட்டமைப்பை வரையறுத்தல்
முதலில், நமது நோட் வகைகளை வரையறுப்போம். ஒரு விர்ச்சுவல் `accept` மெத்தடுடன் கூடிய பாரம்பரிய மரபுவழி படிநிலைக்கு பதிலாக, நமது நோட்களை எளிய ஸ்டிரக்ட்களாக வரையறுப்போம். பின்னர் நமது நோட் வகைகளில் ஏதேனும் ஒன்றைக் கொண்டிருக்கக்கூடிய ஒரு சம் வகையை உருவாக்க `std::variant`-ஐப் பயன்படுத்துவோம்.
ஒரு ரெக்கர்சிவ் கட்டமைப்பை அனுமதிக்க (நோட்கள் மற்ற நோட்களைக் கொண்டிருக்கும் ஒரு ட்ரீ), நமக்கு ஒரு மறைமுக அடுக்கு தேவை. ஒரு `Node` ஸ்டிரக்ட் வேரியண்ட்டைச் சுற்றி வளைத்து, அதன் பிள்ளைகளுக்கு `std::unique_ptr`-ஐப் பயன்படுத்தும்.
கோப்பு: `Nodes.h`
#include <memory> #include <variant> #include <vector> // முக்கிய Node ரேப்பரை முன்னோக்கி அறிவிக்கவும் struct Node; // கான்கிரீட் நோட் வகைகளை எளிய தரவுக் கூறுகளாக வரையறுக்கவும் struct NumberNode { double value; }; struct BinaryOpNode { enum class Operator { Add, Subtract, Multiply, Divide }; Operator op; std::unique_ptr<Node> left; std::unique_ptr<Node> right; }; struct UnaryOpNode { enum class Operator { Negate }; Operator op; std::unique_ptr<Node> operand; }; // சாத்தியமான அனைத்து நோட் வகைகளின் சம் வகையை உருவாக்க std::variant-ஐப் பயன்படுத்தவும் using NodeVariant = std::variant<NumberNode, BinaryOpNode, UnaryOpNode>; // வேரியண்ட்டைச் சுற்றியுள்ள முக்கிய Node ஸ்டிரக்ட் struct Node { NodeVariant var; };
இந்தக் கட்டமைப்பு ஏற்கனவே ஒரு பெரிய முன்னேற்றம். நோட் வகைகள் சாதாரண பழைய தரவு ஸ்டிரக்ட்கள். அவற்றுக்கு விசிட்டர்கள் அல்லது எந்த செயல்பாடுகளையும் பற்றிய அறிவு இல்லை. ஒரு `FunctionCallNode`-ஐச் சேர்க்க, நீங்கள் வெறுமனே ஸ்டிரக்டை வரையறுத்து அதை `NodeVariant` மாற்றுப்பெயரில் சேர்க்கிறீர்கள். இது டேட்டா ஸ்ட்ரக்சருக்கான ஒரு ஒற்றைப் புள்ளி மாற்றமாகும்.
படி 2: `std::visit`-ஐக் கொண்டு ஒரு ஜெனரிக் விசிட்டரை உருவாக்குதல்
`std::visit` பயன்பாடு இந்த பேட்டர்னின் அடித்தளமாகும். இது அழைக்கக்கூடிய ஒரு பொருளை (ஒரு ஃபங்ஷன், லேம்டா, அல்லது `operator()` கொண்ட ஒரு பொருள் போன்றவை) மற்றும் ஒரு `std::variant`-ஐ எடுத்து, வேரியண்ட்டில் தற்போது செயலில் உள்ள வகையின் அடிப்படையில் அழைக்கக்கூடிய சரியான ஓவர்லோடை செயல்படுத்துகிறது. இது நமது டைப்-சேஃப், கம்பைல்-டைம் டபுள் டிஸ்பாட்ச் பொறிமுறையாகும்.
ஒரு விசிட்டர் இப்போது வேரியண்ட்டில் உள்ள ஒவ்வொரு வகைக்கும் ஒரு ஓவர்லோட் செய்யப்பட்ட `operator()` கொண்ட ஒரு ஸ்டிரக்ட் ஆகும்.
இதைச் செயல்பாட்டில் காண ஒரு எளிய பிரட்டி-பிரிண்டர் விசிட்டரை உருவாக்குவோம்.
கோப்பு: `PrettyPrinter.h`
#include "Nodes.h" #include <string> #include <iostream> struct PrettyPrinter { // NumberNode-க்கான ஓவர்லோட் void operator()(const NumberNode& node) const { std::cout << node.value; } // UnaryOpNode-க்கான ஓவர்லோட் void operator()(const UnaryOpNode& node) const { std::cout << "(- "; std::visit(*this, node.operand->var); // ரெக்கர்சிவ் விசிட் std::cout << ")"; } // BinaryOpNode-க்கான ஓவர்லோட் void operator()(const BinaryOpNode& node) const { std::cout << "("; std::visit(*this, node.left->var); // ரெக்கர்சிவ் விசிட் switch (node.op) { case BinaryOpNode::Operator::Add: std::cout << " + "; break; case BinaryOpNode::Operator::Subtract: std::cout << " - "; break; case BinaryOpNode::Operator::Multiply: std::cout << " * "; break; case BinaryOpNode::Operator::Divide: std::cout << " / "; break; } std::visit(*this, node.right->var); // ரெக்கர்சிவ் விசிட் std::cout << ")"; } };
இங்கே என்ன நடக்கிறது என்பதைக் கவனியுங்கள். டிராவர்சல் தர்க்கம் (பிள்ளைகளைப் பார்வையிடுதல்) மற்றும் செயல்பாட்டு தர்க்கம் (அடைப்புக்குறிகள் மற்றும் ஆபரேட்டர்களை அச்சிடுதல்) `PrettyPrinter`-க்குள் ஒன்றாகக் கலக்கப்பட்டுள்ளன. இது செயல்படும், ஆனால் நாம் இன்னும் சிறப்பாகச் செய்ய முடியும். நாம் என்ன என்பதை எப்படி என்பதிலிருந்து பிரிக்கலாம்.
படி 3: நிகழ்ச்சியின் நட்சத்திரம் - ஜெனரிக் ட்ரீ டிராவர்சல் விசிட்டர்
இப்போது, முக்கிய கருத்தை அறிமுகப்படுத்துகிறோம்: டிராவர்சல் உத்தியை உள்ளடக்கிய ஒரு மறுபயன்பாட்டுக்குரிய `TreeWalker`. இந்த `TreeWalker` தானாகவே ஒரு விசிட்டராக இருக்கும், ஆனால் அதன் ஒரே வேலை ட்ரீயை கடந்து செல்வதுதான். இது டிராவர்சலின் போது குறிப்பிட்ட புள்ளிகளில் செயல்படுத்தப்படும் பிற ஃபங்ஷன்களை (லேம்டாக்கள் அல்லது ஃபங்ஷன் ஆப்ஜெக்ட்கள்) எடுக்கும்.
நாம் வெவ்வேறு உத்திகளை ஆதரிக்க முடியும், ஆனால் ஒரு பொதுவான மற்றும் சக்திவாய்ந்த ஒன்று "ப்ரீ-விசிட்" (பிள்ளைகளைப் பார்வையிடுவதற்கு முன்) மற்றும் "போஸ்ட்-விசிட்" (பிள்ளைகளைப் பார்வையிட்ட பிறகு) ஆகியவற்றிற்கான ஹூக்குகளை வழங்குவதாகும். இது ப்ரீ-ஆர்டர் மற்றும் போஸ்ட்-ஆர்டர் டிராவர்சல் செயல்களுக்கு நேரடியாகப் பொருந்துகிறது.
கோப்பு: `TreeWalker.h`
#include "Nodes.h" #include <functional> template <typename PreVisitAction, typename PostVisitAction> struct TreeWalker { PreVisitAction pre_visit; PostVisitAction post_visit; // குழந்தைகள் இல்லாத நோட்களுக்கான அடிப்படை நிலை (டெர்மினல்கள்) void operator()(const NumberNode& node) { pre_visit(node); post_visit(node); } // ஒரு குழந்தை உள்ள நோட்களுக்கான நிலை void operator()(const UnaryOpNode& node) { pre_visit(node); std::visit(*this, node.operand->var); // ரெக்கர்ஸ் post_visit(node); } // இரண்டு குழந்தைகள் உள்ள நோட்களுக்கான நிலை void operator()(const BinaryOpNode& node) { pre_visit(node); std::visit(*this, node.left->var); // இடதுபுறம் ரெக்கர்ஸ் std::visit(*this, node.right->var); // வலதுபுறம் ரெக்கர்ஸ் post_visit(node); } }; // வாக்கரை உருவாக்குவதை எளிதாக்க உதவும் ஃபங்ஷன் template <typename Pre, typename Post> auto make_tree_walker(Pre pre, Post post) { return TreeWalker<Pre, Post>{pre, post}; }
இந்த `TreeWalker` பிரிவினையின் ஒரு தலைசிறந்த படைப்பு. இதற்கு அச்சிடுதல், மதிப்பீடு செய்தல் அல்லது வகை சரிபார்த்தல் பற்றி எதுவும் தெரியாது. அதன் ஒரே நோக்கம் ட்ரீயை டெப்த்-ஃபர்ஸ்ட் டிராவர்சல் செய்து, வழங்கப்பட்ட ஹூக்குகளை அழைப்பதாகும். `pre_visit` செயல் ப்ரீ-ஆர்டரில் செயல்படுத்தப்படுகிறது, மற்றும் `post_visit` செயல் போஸ்ட்-ஆர்டரில் செயல்படுத்தப்படுகிறது. எந்த லேம்டாவை செயல்படுத்துவது என்பதைத் தேர்ந்தெடுப்பதன் மூலம், பயனர் எந்த வகையான செயல்பாட்டையும் செய்ய முடியும்.
படி 4: சக்திவாய்ந்த, பிரிக்கப்பட்ட செயல்பாடுகளுக்கு `TreeWalker`-ஐப் பயன்படுத்துதல்
இப்போது, நமது `PrettyPrinter`-ஐ ரீஃபாக்டர் செய்து, நமது புதிய ஜெனரிக் `TreeWalker`-ஐப் பயன்படுத்தி ஒரு `EvaluationVisitor`-ஐ உருவாக்குவோம். செயல்பாட்டு தர்க்கம் இப்போது எளிய லேம்டாக்களாக வெளிப்படுத்தப்படும்.
லேம்டா அழைப்புகளுக்கு இடையில் நிலையை (மதிப்பீட்டு ஸ்டாக் போன்றவை) கடத்த, நாம் மாறிகளை ரெஃபரன்ஸ் மூலம் கேப்சர் செய்யலாம்.
கோப்பு: `main.cpp`
#include "Nodes.h" #include "TreeWalker.h" #include <iostream> #include <string> #include <vector> // எந்த நோட் வகையையும் கையாளக்கூடிய ஒரு ஜெனரிக் லேம்டாவை உருவாக்க உதவும் ஹெல்பர் template<class... Ts> struct Overloaded : Ts... { using Ts::operator()...; }; template<class... Ts> Overloaded(Ts...) -> Overloaded<Ts...>; int main() { // (5 + (10 * 2)) என்ற கோவையின் ஒரு ட்ரீயை உருவாக்குவோம் auto num5 = std::make_unique<Node>(Node{NumberNode{5.0}}); auto num10 = std::make_unique<Node>(Node{NumberNode{10.0}}); auto num2 = std::make_unique<Node>(Node{NumberNode{2.0}}); auto mult = std::make_unique<Node>(Node{BinaryOpNode{ BinaryOpNode::Operator::Multiply, std::move(num10), std::move(num2) }}); auto root = std::make_unique<Node>(Node{BinaryOpNode{ BinaryOpNode::Operator::Add, std::move(num5), std::move(mult) }}); std::cout << "--- பிரட்டி பிரிண்டிங் செயல்பாடு ---\n"; auto printer_pre_visit = Overloaded { [](const NumberNode& node) { std::cout << node.value; }, [](const UnaryOpNode&) { std::cout << "(-"; }, [](const BinaryOpNode&) { std::cout << "("; } }; auto printer_post_visit = Overloaded { [](const NumberNode&) {}, // எதுவும் செய்ய வேண்டாம் [](const UnaryOpNode&) { std::cout << ")"; }, [](const BinaryOpNode& node) { switch (node.op) { case BinaryOpNode::Operator::Add: std::cout << " + "; break; case BinaryOpNode::Operator::Subtract: std::cout << " - "; break; case BinaryOpNode::Operator::Multiply: std::cout << " * "; break; case BinaryOpNode::Operator::Divide: std::cout << " / "; break; } } }; // இது வேலை செய்யாது, ஏனெனில் குழந்தைகள் ப்ரீ மற்றும் போஸ்ட் விசிட்களுக்கு இடையில் பார்வையிடப்படுகின்றன. // இன்-ஆர்டர் பிரிண்டிற்காக வாக்கரை இன்னும் நெகிழ்வானதாக மாற்றுவோம். // பிரட்டி பிரிண்டிங்கிற்கு ஒரு சிறந்த அணுகுமுறை "இன்-விசிட்" ஹூக்கைக் கொண்டிருப்பதாகும். // எளிமைக்காக, பிரிண்டிங் தர்க்கத்தை சற்று மாற்றி அமைப்போம். // அல்லது இன்னும் சிறப்பாக, ஒரு பிரத்யேக PrintWalker-ஐ உருவாக்குவோம். இப்போதைக்கு ப்ரீ/போஸ்ட்டைப் பின்பற்றி, மதிப்பீட்டைக் காண்பிப்போம், அது ஒரு சிறந்த பொருத்தம். std::cout << "\n--- மதிப்பீட்டு செயல்பாடு ---\n"; std::vector<double> eval_stack; auto eval_pre_visit = [](const auto&){}; // ப்ரீ-விசிட்டில் எதுவும் செய்ய வேண்டாம் auto eval_post_visit = Overloaded { [&](const NumberNode& node) { eval_stack.push_back(node.value); }, [&](const UnaryOpNode& node) { double operand = eval_stack.back(); eval_stack.pop_back(); eval_stack.push_back(-operand); }, [&](const BinaryOpNode& node) { double right = eval_stack.back(); eval_stack.pop_back(); double left = eval_stack.back(); eval_stack.pop_back(); switch(node.op) { case BinaryOpNode::Operator::Add: eval_stack.push_back(left + right); break; case BinaryOpNode::Operator::Subtract: eval_stack.push_back(left - right); break; case BinaryOpNode::Operator::Multiply: eval_stack.push_back(left * right); break; case BinaryOpNode::Operator::Divide: eval_stack.push_back(left / right); break; } } }; auto evaluator = make_tree_walker(eval_pre_visit, eval_post_visit); std::visit(evaluator, root->var); std::cout << "மதிப்பீட்டு முடிவு: " << eval_stack.back() << std::endl; return 0; }
மதிப்பீட்டு தர்க்கத்தைப் பாருங்கள். இது ஒரு போஸ்ட்-ஆர்டர் டிராவர்சலுக்கு ஒரு சரியான பொருத்தம். அதன் பிள்ளைகளின் மதிப்புகள் கணக்கிடப்பட்டு ஸ்டாக்கில் சேர்க்கப்பட்ட பின்னரே நாம் ஒரு செயல்பாட்டைச் செய்கிறோம். `eval_post_visit` லேம்டா `eval_stack`-ஐக் கைப்பற்றி மதிப்பீட்டிற்கான அனைத்து தர்க்கத்தையும் கொண்டுள்ளது. இந்த தர்க்கம் நோட் வரையறைகள் மற்றும் `TreeWalker`-இலிருந்து முற்றிலும் பிரிக்கப்பட்டுள்ளது. நாம் கவலைகளின் ஒரு அழகான மூன்று-வழிப் பிரிவை அடைந்துள்ளோம்: டேட்டா ஸ்ட்ரக்சர் (நோட்கள்), டிராவர்சல் அல்காரிதம் (`TreeWalker`), மற்றும் செயல்பாட்டு தர்க்கம் (லேம்டாக்கள்).
ஜெனரிக் விசிட்டர் அணுகுமுறையின் நன்மைகள்
இந்த செயலாக்க உத்தி குறிப்பிடத்தக்க நன்மைகளை வழங்குகிறது, குறிப்பாக பெரிய அளவிலான, நீண்ட காலம் வாழும் மென்பொருள் திட்டங்களில்.
இணையற்ற நெகிழ்வுத்தன்மை மற்றும் விரிவாக்கத்தன்மை
இதுதான் முதன்மை நன்மை. ஒரு புதிய செயல்பாட்டைச் சேர்ப்பது அற்பமானது. நீங்கள் வெறுமனே ஒரு புதிய லேம்டாக்கள் தொகுப்பை எழுதி அவற்றை `TreeWalker`-க்கு அனுப்புகிறீர்கள். நீங்கள் எந்த தற்போதைய குறியீட்டையும் தொடவில்லை. இது திறந்த/மூடிய கொள்கையை முழுமையாகப் பின்பற்றுகிறது. ஒரு புதிய நோட் வகையைச் சேர்ப்பதற்கு ஸ்டிரக்டைச் சேர்ப்பது மற்றும் `std::variant` மாற்றுப்பெயரைப் புதுப்பிப்பது தேவைப்படுகிறது—ஒரு ஒற்றை, உள்ளூர்மயமாக்கப்பட்ட மாற்றம்—பின்னர் அதைக் கையாள வேண்டிய விசிட்டர்களைப் புதுப்பிக்க வேண்டும். எந்த விசிட்டர்களில் (ஓவர்லோட் செய்யப்பட்ட லேம்டாக்கள்) இப்போது ஒரு ஓவர்லோட் இல்லை என்பதை கம்பைலர் உதவியாக உங்களுக்குச் சொல்லும்.
சிறந்த கவலைகளின் பிரிப்பு
நாம் மூன்று தனித்துவமான பொறுப்புகளைத் தனிமைப்படுத்தியுள்ளோம்:
- தரவு பிரதிநிதித்துவம்: `Node` ஸ்டிரக்ட்கள் எளிய, செயலற்ற தரவுக் கொள்கலன்கள்.
- டிராவர்சல் மெக்கானிக்ஸ்: `TreeWalker` வகுப்பு ட்ரீ கட்டமைப்பை எவ்வாறு வழிநடத்துவது என்பதற்கான தர்க்கத்தை பிரத்தியேகமாகக் கொண்டுள்ளது. கணினியின் வேறு எந்தப் பகுதியையும் மாற்றாமல் நீங்கள் எளிதாக ஒரு `InOrderTreeWalker` அல்லது `BreadthFirstTreeWalker`-ஐ உருவாக்கலாம்.
- செயல்பாட்டு தர்க்கம்: வாக்கருக்கு அனுப்பப்பட்ட லேம்டாக்கள் ஒரு குறிப்பிட்ட பணிக்கான (மதிப்பீடு செய்தல், அச்சிடுதல், வகை சரிபார்த்தல் போன்றவை) குறிப்பிட்ட வணிக தர்க்கத்தைக் கொண்டுள்ளன.
இந்தப் பிரிப்பு குறியீட்டைப் புரிந்துகொள்வதற்கும், சோதிப்பதற்கும், பராமரிப்பதற்கும் எளிதாக்குகிறது. ஒவ்வொரு கூறுக்கும் ஒரு ஒற்றை, நன்கு வரையறுக்கப்பட்ட பொறுப்பு உள்ளது.
மேம்படுத்தப்பட்ட மறுபயன்பாடு
`TreeWalker` எல்லையற்ற முறையில் மறுபயன்பாட்டுக்குரியது. டிராவர்சல் தர்க்கம் ஒரு முறை எழுதப்பட்டு வரம்பற்ற எண்ணிக்கையிலான செயல்பாடுகளுக்குப் பயன்படுத்தப்படலாம். இது குறியீட்டின் நகலெடுப்பையும், ஒவ்வொரு புதிய விசிட்டரிலும் டிராவர்சல் தர்க்கத்தை மீண்டும் செயல்படுத்துவதால் ஏற்படக்கூடிய பிழைகளின் சாத்தியத்தையும் குறைக்கிறது.
சுருக்கமான மற்றும் வெளிப்படையான குறியீடு
நவீன சி++ அம்சங்களுடன், இதன் விளைவாக வரும் குறியீடு பெரும்பாலும் கிளாசிக் விசிட்டர் செயலாக்கங்களை விட சுருக்கமாக இருக்கும். லேம்டாக்கள் செயல்பாட்டு தர்க்கத்தை அது பயன்படுத்தப்படும் இடத்திலேயே வரையறுக்க அனுமதிக்கின்றன, இது எளிய, உள்ளூர்மயமாக்கப்பட்ட செயல்பாடுகளுக்குப் படிக்கும் திறனை மேம்படுத்தும். ஒரு லேம்டாக்கள் தொகுப்பிலிருந்து விசிட்டர்களை உருவாக்குவதற்கான `Overloaded` ஹெல்பர் ஸ்டிரக்ட் என்பது ஒரு பொதுவான மற்றும் சக்திவாய்ந்த மொழிநடையாகும், இது விசிட்டர் வரையறைகளை சுத்தமாக வைத்திருக்கிறது.
சாத்தியமான பரிமாற்றங்கள் மற்றும் பரிசீலனைகள்
எந்த பேட்டர்னும் ஒரு வெள்ளித் தோட்டா அல்ல. சம்பந்தப்பட்ட பரிமாற்றங்களைப் புரிந்துகொள்வது முக்கியம்.
ஆரம்ப அமைப்பின் சிக்கல்தன்மை
`std::variant` மற்றும் ஜெனரிக் `TreeWalker`-உடன் கூடிய `Node` கட்டமைப்பின் ஆரம்ப அமைப்பு, ஒரு நேரடியான ரெக்கர்சிவ் ஃபங்ஷன் அழைப்பை விட சிக்கலானதாக உணரப்படலாம். இந்த பேட்டர்ன், ட்ரீ கட்டமைப்பு நிலையானதாக இருக்கும் ஆனால் செயல்பாடுகளின் எண்ணிக்கை காலப்போக்கில் வளரும் என்று எதிர்பார்க்கப்படும் கணினிகளில் அதிக நன்மையை வழங்குகிறது. மிகவும் எளிமையான, ஒரு முறை மட்டுமேயான ட்ரீ செயலாக்கப் பணிகளுக்கு, இது மிகையாக இருக்கலாம்.
செயல்திறன்
சி++ இல் `std::visit`-ஐப் பயன்படுத்தி இந்த பேட்டர்னின் செயல்திறன் சிறப்பானது. `std::visit` பொதுவாக கம்பைலர்களால் மிகவும் உகந்ததாக்கப்பட்ட ஜம்ப் டேபிளைப் பயன்படுத்தி செயல்படுத்தப்படுகிறது, இது டிஸ்பாட்ச்சை மிகவும் வேகமாக ஆக்குகிறது—பெரும்பாலும் விர்ச்சுவல் ஃபங்ஷன் அழைப்புகளை விட வேகமாக. இதே போன்ற ஜெனரிக் நடத்தையை அடைய பிரதிபலிப்பு அல்லது அகராதி அடிப்படையிலான வகை தேடல்களை நம்பியிருக்கக்கூடிய பிற மொழிகளில், ஒரு கிளாசிக், நிலையாக-டிஸ்பாட்ச் செய்யப்பட்ட விசிட்டருடன் ஒப்பிடும்போது ஒரு குறிப்பிடத்தக்க செயல்திறன் மேல்செலவு இருக்கலாம்.
மொழி சார்பு
இந்த குறிப்பிட்ட செயலாக்கத்தின் நேர்த்தியும் திறனும் சி++17 அம்சங்களை பெரிதும் சார்ந்துள்ளன. கொள்கைகள் மாற்றத்தக்கவை என்றாலும், மற்ற மொழிகளில் செயலாக்க விவரங்கள் வேறுபடும். உதாரணமாக, ஜாவாவில், நவீன பதிப்புகளில் ஒரு சீல்டு இன்டர்ஃபேஸ் மற்றும் பேட்டர்ன் மேட்சிங்கைப் பயன்படுத்தலாம், அல்லது பழைய பதிப்புகளில் ஒரு விரிவான மேப்-அடிப்படையிலான டிஸ்பாட்ச்சரைப் பயன்படுத்தலாம்.
நிஜ-உலகப் பயன்பாடுகள் மற்றும் பயன்பாட்டு வழக்குகள்
ட்ரீ டிராவர்சலுக்கான ஜெனரிக் விசிட்டர் பேட்டர்ன் ஒரு கல்விப் பயிற்சி மட்டுமல்ல; இது பல சிக்கலான மென்பொருள் அமைப்புகளின் முதுகெலும்பாகும்.
- கம்பைலர்கள் மற்றும் இன்டர்பிரிட்டர்கள்: இது ஒரு உன்னதமான பயன்பாட்டு வழக்கு. ஒரு அப்ஸ்ட்ராக்ட் சின்டாக்ஸ் ட்ரீ (AST) வெவ்வேறு "விசிட்டர்கள்" அல்லது "பாஸ்கள்" மூலம் பலமுறை கடந்து செல்லப்படுகிறது. ஒரு செமண்டிக் அனாலிசிஸ் பாஸ் வகை பிழைகளைச் சரிபார்க்கிறது, ஒரு ஆப்டிமைசேஷன் பாஸ் ட்ரீயை மேலும் திறமையானதாக மாற்றியமைக்கிறது, மற்றும் ஒரு கோட் ஜெனரேஷன் பாஸ் இறுதி ட்ரீயைக் கடந்து மெஷின் கோட் அல்லது பைட் கோடை வெளியிடுகிறது. ஒவ்வொரு பாஸும் ஒரே டேட்டா ஸ்ட்ரக்சரில் ஒரு தனித்துவமான செயல்பாடாகும்.
- ஸ்டேடிக் அனாலிசிஸ் கருவிகள்: லின்டர்கள், கோட் ஃபார்மேட்டர்கள், மற்றும் பாதுகாப்பு ஸ்கேனர்கள் போன்ற கருவிகள் குறியீட்டை ஒரு AST ஆகப் பிரித்து, பின்னர் பேட்டர்ன்களைக் கண்டறியவும், ஸ்டைல் விதிகளை அமல்படுத்தவும், அல்லது சாத்தியமான பாதிப்புகளைக் கண்டறியவும் அதன் மீது பல்வேறு விசிட்டர்களை இயக்குகின்றன.
- ஆவண செயலாக்கம் (DOM): நீங்கள் ஒரு XML அல்லது HTML ஆவணத்தைக் கையாளும்போது, நீங்கள் ஒரு ட்ரீயுடன் வேலை செய்கிறீர்கள். அனைத்து இணைப்புகளையும் பிரித்தெடுக்க, அனைத்து படங்களையும் மாற்ற, அல்லது ஆவணத்தை வேறு வடிவத்திற்கு வரிசைப்படுத்த ஒரு ஜெனரிக் விசிட்டரைப் பயன்படுத்தலாம்.
- UI ஃபிரேம்வொர்க்குகள்: நவீன UI ஃபிரேம்வொர்க்குகள் பயனர் இடைமுகத்தை ஒரு கூறு ட்ரீயாகக் குறிக்கின்றன. இந்த ட்ரீயைக் கடந்து செல்வது ரெண்டரிங், நிலை புதுப்பிப்புகளைப் பரப்புதல் (ரியாக்டின் சமரச அல்காரிதத்தில் உள்ளது போல), அல்லது நிகழ்வுகளை டிஸ்பாட்ச் செய்வதற்கு அவசியமானது.
- 3D கிராஃபிக்ஸில் சீன் கிராஃப்கள்: ஒரு 3D காட்சி பெரும்பாலும் பொருட்களின் ஒரு படிநிலையாகக் குறிக்கப்படுகிறது. மாற்றங்களைப் பயன்படுத்தவும், இயற்பியல் உருவகப்படுத்துதல்களைச் செய்யவும், மற்றும் ரெண்டரிங் பைப்லைனுக்கு பொருட்களைச் சமர்ப்பிக்கவும் ஒரு டிராவர்சல் தேவைப்படுகிறது. ஒரு ஜெனரிக் வாக்கர் ஒரு ரெண்டரிங் செயல்பாட்டைப் பயன்படுத்தலாம், பின்னர் ஒரு இயற்பியல் புதுப்பிப்பு செயல்பாட்டைப் பயன்படுத்த மீண்டும் பயன்படுத்தப்படலாம்.
முடிவு: ஒரு புதிய நிலை சுருக்கம்
ஜெனரிக் விசிட்டர் பேட்டர்ன், குறிப்பாக ஒரு பிரத்யேக `TreeWalker`-உடன் செயல்படுத்தப்படும்போது, மென்பொருள் வடிவமைப்பில் ஒரு சக்திவாய்ந்த பரிணாமத்தைக் குறிக்கிறது. இது விசிட்டர் பேட்டர்னின் அசல் வாக்குறுதியான—தரவு மற்றும் செயல்பாடுகளின் பிரிப்பு—எடுத்துக்கொண்டு, டிராவர்சலின் சிக்கலான தர்க்கத்தையும் பிரிப்பதன் மூலம் அதை உயர்த்துகிறது.
சிக்கலை மூன்று தனித்துவமான, செங்குத்தான கூறுகளாக—தரவு, டிராவர்சல், மற்றும் செயல்பாடு—உடைப்பதன் மூலம், நாம் மேலும் மட்டுப்படுத்தப்பட்ட, பராமரிக்கக்கூடிய, மற்றும் வலுவான அமைப்புகளை உருவாக்குகிறோம். முக்கிய தரவு கட்டமைப்புகள் அல்லது டிராவர்சல் குறியீட்டை மாற்றியமைக்காமல் புதிய செயல்பாடுகளைச் சேர்க்கும் திறன் மென்பொருள் கட்டமைப்பிற்கு ஒரு மகத்தான வெற்றியாகும். `TreeWalker` என்பது ஒரு மறுபயன்பாட்டுக்குரிய சொத்தாக மாறுகிறது, இது டஜன் கணக்கான அம்சங்களை இயக்க முடியும், டிராவர்சல் தர்க்கம் அது பயன்படுத்தப்படும் எல்லா இடங்களிலும் சீரானதாகவும் சரியாகவும் இருப்பதை உறுதி செய்கிறது.
புரிந்துகொள்வதற்கும் அமைப்பதற்கும் ஆரம்பத்தில் ஒரு முதலீடு தேவைப்பட்டாலும், ஜெனரிக் ட்ரீ டிராவர்சல் விசிட்டர் பேட்டர்ன் ஒரு திட்டத்தின் வாழ்க்கை முழுவதும் பலன்களைத் தருகிறது. சிக்கலான படிநிலைத் தரவுகளுடன் பணிபுரியும் எந்தவொரு டெவலப்பருக்கும், இது சுத்தமான, நெகிழ்வான, மற்றும் நீடித்த குறியீட்டை எழுதுவதற்கான ஒரு அத்தியாவசிய கருவியாகும்.